Blog

Push notification fallbacks: email, Slack, and SMS

Kyle SeylerKyle SeylerSeptember 16, 2025
Push Fallback, SMS, Email, Slack

Push notification fallbacks give a message somewhere else to go when push can't deliver it. In Courier, that's one routing object on the send: try push first, then email, then SMS, and stop at the first channel that works. A user without a push token still gets the email, and you don't write any retry or channel-switching code.

Courier handles fallback at two levels. Channel fallback moves the message to a different channel, like push to email. Provider failover switches to a backup service inside the same channel, like Amazon SES to SendGrid. You can use either one, or both on the same send.

When do you need push notification fallbacks?

Common scenarios where push fails

Push depends on a valid device token and a working push provider. Courier handles each failure like this:

  • The app was uninstalled. The device token stops working, and the push provider rejects the send. Courier treats that as a provider error, the push channel fails, and a single route moves to the next channel.
  • The token is missing or expired. If the user has no push token on their profile, Courier has no address for the push channel, so the channel fails right away and the route moves on. An expired token gets rejected by the provider, with the same result.
  • Firebase Cloud Messaging (FCM) or Apple Push Notification service (APNs) is down. Courier retries transient errors before giving up on a provider. When the push channel fails, the route moves to your backup channel.
  • Notifications are turned off. If your app only saves a token to Courier after the user grants permission, a user who says no has no token, and Courier skips push. If they turn notifications off later, the provider can still accept the send. That's a successful send, not a failure, so remove the token when your app sees permission revoked.

Types of messages that need fallbacks

Not every notification needs a backup channel. Put fallbacks on the ones where a missed message costs the user something:

  • Security messages: password resets, new sign-in alerts, and verification codes
  • Transactional messages: receipts, payment failures, and order or shipping updates
  • Time-sensitive messages: appointment reminders, delivery windows, and expiring links

How do you configure channel routing in Courier?

Every Courier message routes in two steps. It picks channels from routing.channels, then picks providers inside each channel. Each step has a method:

DecisionSet withsingleall
Which channelsrouting.methodTries channels in order, stops at the first that deliversSends on every channel in the list
Which providersThe channel's routing_methodTries providers in order, stops at the first that delivers (the default)Sends through every provider on the channel

Under single, everything after the first option is its backup.

Fall back from push to email and SMS

This send tries push, then email, then SMS. A user with no push token gets the email, and SMS only goes out if email fails too:

JavaScript
import Courier from "@trycourier/courier";

const client = new Courier({ apiKey: process.env.COURIER_API_KEY });

const { requestId } = await client.send.message({
  message: {
    to: { user_id: "user_123" },
    template: "YOUR_TEMPLATE_ID",
    routing: {
      method: "single",
      channels: ["push", "email", "sms"],
    },
  },
});

Route to channel names like push and email, not provider keys. Courier recommends this because it keeps provider failover and channel priority working inside each channel. You can name a single provider instead, but then Courier sends through that one provider only. See Send to a provider for when that's worth it.

Reach every device on push

FCM and APNs are two providers inside the one push channel, and a channel's routing_method defaults to single. So a user with an iPhone and an Android tablet gets the push on whichever device Courier reaches first. To push to both, set the push channel's routing_method to all. It goes in message.channels, not in routing:

JavaScript
const { requestId } = await client.send.message({
  message: {
    to: { user_id: "user_123" },
    template: "YOUR_TEMPLATE_ID",
    routing: {
      method: "single",
      channels: ["push", "email"],
    },
    channels: {
      push: { routing_method: "all" },
    },
  },
});

Install both APNs and Firebase FCM under Integrations so Courier can use them. A user with only an iPhone gets the same result either way, because Courier skips FCM when there's no FCM token. If push fails on every provider, the route still falls back to email.

Fall back to Slack or Microsoft Teams

For internal alerts, workplace chat is often a better backup than email. This send tries push, then Slack, then email:

JavaScript
const { requestId } = await client.send.message({
  message: {
    to: { user_id: "user_123" },
    template: "YOUR_TEMPLATE_ID",
    routing: {
      method: "single",
      channels: ["push", "slack", "email"],
    },
  },
});

Here slack is the Slack provider key, so that step goes through Slack specifically. Courier reads the Slack target (a channel, user ID, or email, plus your bot token) from the user's profile. The Slack integration docs cover the profile shape, and Microsoft Teams works the same way with its own provider.

Always send one channel, fall back across the rest

An entry in channels can be a nested routing object with its own method. Here the Inbox message always goes out, and the user also gets push, or email if push can't deliver:

JSON
{
  "method": "all",
  "channels": [
    "inbox",
    { "method": "single", "channels": ["push", "email"] }
  ]
}

Save it as a routing strategy

You don't have to put routing on every send. A routing strategy saves the channel order, fallback, and providers once, and every template that uses it picks up your edits on its next send. Build one in Design Studio or with the API. An inline routing object on a send replaces the strategy's channel list and method for that one message, while the strategy's provider order and channel conditions still apply.

How do you handle provider failures?

Channel fallback covers the user's side. Provider failover covers yours: if your email provider can't take the message, Courier switches to a backup provider on the same channel automatically.

List the providers for a channel in priority order. This send uses Amazon SES first and SendGrid as the backup:

JavaScript
const { requestId } = await client.send.message({
  "Idempotency-Key": "order-1234-shipped",
  message: {
    to: { user_id: "user_123" },
    template: "YOUR_TEMPLATE_ID",
    routing: {
      method: "single",
      channels: ["email"],
    },
    channels: {
      email: {
        providers: ["ses", "sendgrid"],
        routing_method: "single",
      },
    },
  },
});

Courier moves to the next provider when one can't take the message or returns an error. It retries transient errors, like a rate limit or a dropped connection, before moving on. Every attempt shows in the message's log, so you can see which provider delivered.

Retrying with the same Idempotency-Key returns the original response instead of sending again. Use one key per logical event, like one order shipping.

Timeouts for slow providers

Failover on errors works on every plan. On an Enterprise plan, Courier also moves past a provider or channel that's slow rather than failed. It gives each provider 5 minutes and each channel 30 minutes by default, and you can change both per send. On other plans, a send that sets provider or channel timeouts returns 402. The timeouts reference has the details.

How do preferences affect fallbacks?

A backup channel never gets around a user's opt-out. Courier resolves preferences on top of routing, at send time:

  • Opted out: if the user opted out of the message's subscription topic, the send is blocked, and the log shows the reason UNSUBSCRIBED. Courier doesn't reroute around it.
  • Custom routing: if the user picked their own channels for a topic (custom_routing) and the topic has channel selection turned on, their channels replace the template's routing.
  • Required topics: a topic with a default of REQUIRED always delivers, and users can't opt out of it. Use it for security alerts and other messages that have to reach someone.

Send to a stable user_id and map the template to the right topic, and Courier applies all of this for you.

Sent, delivered, and ignored are different things

Fallback fires when a channel fails, not when a user ignores a message. If APNs accepts a push and the user never opens it, the send succeeded and the route is done.

Two statuses matter here. SENT means Courier handed the message to the provider. DELIVERED means the provider confirmed it landed, which only happens once delivery tracking is set up for that provider. Neither one tells you someone read it.

For "remind them by email if they haven't finished setup," use Courier Journeys. Add a delay node, fetch the user's current state, and branch: send the email only if setup is still incomplete.

Test your fallback route

Use test users and check the log for each case:

TestWhat you should see
User has a push tokenPush sends, and no email goes out under single
User has no push token, has an emailPush channel fails, and the email sends
Primary email provider failsBackup provider takes the message
User opted out of the topicSend blocked with UNSUBSCRIBED, no fallback
User set custom routing for the topicTheir channels are used, not the template's

Comparing platforms? Automatic provider failover is built into Courier. See Courier vs Novu and Courier vs Customer.io.

Frequently asked questions

What happens if every channel fails?

If every option in the route fails, the message fails. Every attempt, on every channel and provider, shows in the message's log, so you can see exactly where it stopped.

Does fallback trigger when a user ignores a push notification?

No. If the push provider accepts the message, the send succeeded. To follow up with email when a user doesn't act, build that in Journeys with a delay and a branch on the user's current state.

Can a fallback channel reach a user who opted out?

No. An opt-out blocks the send for that topic, and Courier doesn't reroute around it. If a message has to reach everyone, map it to a topic with a REQUIRED default.

How do I send one push to both iOS and Android?

Set the push channel's routing_method to all in message.channels, and install both APNs and Firebase FCM. Courier then sends through every push provider the user has a token for.

Is failover available on every plan?

Yes. Courier fails over between providers and channels on errors on every plan. Moving past a provider or channel that's slow, rather than failed, requires an Enterprise plan.

Should I route to providers or channels?

Channels. Routing to push or email keeps provider failover working inside each channel. Name a provider key like slack only when you want that one provider specifically.

Start routing with fallbacks

Pick your most important notification, add routing: { method: "single", channels: ["push", "email"] }, and send it to a test user without a push token. The routing guide covers every pattern in this post. Sign up free to try it.